梦想不会自己发光,真正闪耀的是那个为梦狂奔的你。献给知行的孩子们!(Eric.He著)
分治算法(Divide and Conquer),字面意思是 “分而治之”,是一种经典的算法思想。将一个规模庞大、难以直接解决的复杂原问题,按照一定的规则分解为若干个相互独立、结构与原问题相似但规模更小的子问题;递归或迭代地求解这些子问题;最后按照特定的策略将子问题的解合并起来,最终得到原问题的完整解。
分治算法的执行过程可概括为 “三步走”,这三步构成了分治的核心逻辑,缺一不可:
简单来说,分治算法就是 “把大问题拆成小问题解决,再把小问题的答案拼起来”,类似于 “拆解复杂任务,分工完成后汇总结果” 的工作模式。
分治算法有效执行的四个必要前提:
要实现一个高效的分治算法,通常遵循以下 4 个标准步骤:
明确原问题的求解目标,设计合理的拆分规则,将原问题分解为若干个规模均匀、结构相似的子问题。例如:
明确子问题的最小规模边界,当子问题规模缩小到该边界(基线条件)时,无需再拆分,直接通过简单方法求解。例如:
对每个拆分后的子问题,递归调用自身的求解逻辑,逐个得到子问题的解。这一步是分治算法的 “核心执行层”,依赖递归的自调用特性,逐步缩小问题规模。例如:
根据原问题的需求,设计对应的合并规则,将所有子问题的解整合起来,形成原问题的最终解。这一步是分治算法的 “灵魂”,直接决定算法的效率和正确性。例如:
分治算法的优化核心围绕 “拆分效率” 和 “合并效率” 展开,同时规避潜在问题,常见优化方向有:
分治算法适用于具有自相似性、子问题独立且可合并的大规模问题,常见包括:
下面以 “归并排序” 为例,展示分治算法的实现。归并排序是分治算法的经典代表 —— 核心是 “分(拆分数组)+ 治(递归排序子数组)+ 合(合并有序子数组)”。
算法解析
#include <iostream>
#include <iostream>
#include <vector>
using namespace std;
// 合并操作:将两个有序子数组[left, mid]和[mid+1, right]合并为一个有序数组
// arr:原数组
// temp:临时数组(用于存储合并结果,避免频繁创建数组)
// left:左子数组起始索引
// mid:拆分点(左子数组结束索引)
// right:右子数组结束索引
void merge(vector<int>& arr, vector<int>& temp, int left, int mid, int right) {
// 1. 初始化指针
int i = left; // 左子数组指针
int j = mid + 1; // 右子数组指针
int k = left; // 临时数组指针
// 2. 合并两个有序子数组到临时数组
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 3. 处理左子数组剩余元素
while (i <= mid) {
temp[k++] = arr[i++];
}
// 4. 处理右子数组剩余元素
while (j <= right) {
temp[k++] = arr[j++];
}
// 5. 将临时数组的有序结果复制回原数组
for (int idx = left; idx <= right; idx++) {
arr[idx] = temp[idx];
}
}
// 分治实现归并排序(递归版)
// arr:待排序数组
// temp:临时数组(复用,减少内存开销)
// left:当前排序区间起始索引
// right:当前排序区间结束索引
void sort_recu(vector<int>& arr, vector<int>& temp, int left, int right) {
// 递归终止条件:区间只有1个元素或空(天然有序)
if (left >= right) {
return;
}
// 1. 分:计算中间点,拆分为左、右子数组
int mid = left + (right - left) / 2; // 避免(left+right)溢出
// 2. 治:递归排序左、右子数组
sort_recu(arr, temp, left, mid); // 排序左区间[left, mid]
sort_recu(arr, temp, mid + 1, right); // 排序右区间[mid+1, right]
// 3. 合:合并两个有序子数组
merge(arr, temp, left, mid, right);
}
// 包装函数:简化调用(无需手动传入temp、left、right)
void sort_recu(vector<int>& arr) {
if (arr.empty() || arr.size() == 1) {
return; // 空数组或单元素数组无需排序
}
vector<int> temp(arr.size()); // 创建临时数组(仅创建一次,复用)
sort_recu(arr, temp, 0, arr.size() - 1);
}
// 迭代版归并排序
void sort_iter(vector<int>& arr) {
int n = arr.size();
vector<int> temp(arr.size()); // 创建临时数组(仅创建一次,复用)
// 子数组大小从1开始,每次翻倍
for (int current_size = 1; current_size < n; current_size *= 2) {
// 遍历数组,合并相邻的两个子数组
for (int left = 0; left < n - 1; left += 2 * current_size) {
// 计算左子数组的结束位置
int mid = std::min(left + current_size - 1, n - 1);
// 计算右子数组的结束位置
int right = std::min(left + 2 * current_size - 1, n - 1);
// 合并两个子数组
merge(arr, temp, left, mid, right);
}
}
}
// 辅助函数:打印数组
void printArray(const vector<int>& arr) {
for (int num : arr) {
cout << num << " ";
}
cout << endl;
}
int main() {
// 测试数组
vector<int> arr1 = {8, 4, 5, 7, 1, 3, 6, 2};
vector<int> arr2 = {8, 4, 5, 7, 1, 3, 6, 2};
cout << "排序前数组:";
printArray(arr1);
cout << "排序前数组:";
printArray(arr2);
// 归并排序(迭代)
sort_iter(arr1);
// 归并排序(递归)
sort_recu(arr2);
cout << "迭代排序后数组:";
printArray(arr1);
cout << "递归排序后数组:";
printArray(arr2);
return 0;
}
排序前数组:8 4 5 7 1 3 6 2
排序前数组:8 4 5 7 1 3 6 2
迭代排序后数组:1 2 3 4 5 6 7 8
递归排序后数组:1 2 3 4 5 6 7 8
优点
缺点
| 对比维度 | 分治算法 | 枚举算法 |
|---|---|---|
| 核心思想 | 分而治之(分 + 治 + 合) | 穷举解空间(遍历 + 验证) |
| 依赖关 | 可依赖递归(或迭代)实现 | 依赖循环遍历 |
| 子问题特性 | 子问题独立、结构相似 | 无明确子问题,仅候选解 |
| 关键步骤 | 合并子解(核心) | 验证条件(核心) |
| 时间复杂度 | 常较低(O (n log n)) | 通常较高(O (n²) 及以上) |
| 空间复杂度 | 可能需要额外辅助空间 | 通常较低(无需额外空间) |